﻿#region Using Statements
using System;
using Microsoft.Xna.Framework;
#endregion

namespace StarFox.ScreenComponents
{
    /// <summary>
    /// Implementation of the StarShip's rotation control.
    /// </summary>
    internal class StarShipRotation
    {
        #region Fields

        IShipInput input;
        PlayerIndex playerIndex;
        Matrix rotMatrix = Matrix.Identity;

        #endregion

        #region Yaw

        // total yaw rotation angle
        float yawAngle = 0.0f;

        /// <summary>
        /// Sets the StarShip's maximum yaw rotation angle.
        /// </summary>
        public float MaxYawAngle
        {
            private get;
            set;
        }

        /// <summary>
        /// Sets the time needed by the StarShip to reach its maximum yaw rotation angle when forced by a player input action.
        /// </summary>
        public TimeSpan PlayYawTime
        {
            private get;
            set;
        }

        /// <summary>
        /// Sets the time needed by the StarShip to reach back its null yaw rotation angle when idle.
        /// </summary>
        public TimeSpan IdleYawTime
        {
            private get;
            set;
        }

        #endregion

        #region Pitch

        // total pitch rotation angle
        float pitchAngle = 0.0f;

        /// <summary>
        /// Sets the StarShip's maximum pitch rotation angle.
        /// </summary>
        public float MaxPitchAngle
        {
            private get;
            set;
        }

        /// <summary>
        /// Sets the time needed by the StarShip to reach its maximum pitch rotation angle when forced by a player input action.
        /// </summary>
        public TimeSpan PlayPitchTime
        {
            private get;
            set;
        }

        /// <summary>
        /// Sets the time needed by the StarShip to reach back its null pitch rotation angle when idle.
        /// </summary>
        public TimeSpan IdlePitchTime
        {
            private get;
            set;
        }

        #endregion

        #region Roll

        // partial roll rotation angle
        float rollAngle = 0.0f;

        /// <summary>
        /// Sets the StarShip's maximum roll rotation angle when forced by a "rotate around roll axis" input action.
        /// </summary>
        public float MaxRollAngle
        {
            private get;
            set;
        }

        /// <summary>
        /// Sets the StarShip's maximum roll rotation angle when forced by a "rotate around yaw axis" input action.
        /// </summary>
        public float MaxRollYawAngle
        {
            private get;
            set;
        }

        /// <summary>
        /// Sets the time needed by the StarShip to reach its maximum roll rotation angle when forced by a player input action.
        /// </summary>
        public TimeSpan PlayRollTime
        {
            private get;
            set;
        }

        /// <summary>
        /// Sets the time needed by the StarShip to reach back its null roll rotation angle when idle.
        /// </summary>
        public TimeSpan IdleRollTime
        {
            private get;
            set;
        }

        #endregion

        #region RockRoll

        enum RockRollState
        {
            RR_RIGHT,
            RR_LEFT
        };

        RockRollState rrState = RockRollState.RR_RIGHT;

        // total rocking angle
        float rockAngle = 0.0f;

        /// <summary>
        /// Sets the StarShip's maximum rocking angle.
        /// </summary>
        public float RockRollAngle
        {
            private get;
            set;
        }

        /// <summary>
        /// Sets time needed by the StarShip to reach its maximum rocking angle.
        /// </summary>
        public TimeSpan RockRollTime
        {
            private get;
            set;
        }

        #endregion

        #region Camera Shake

        bool isShake;
        float shakeMagnitude;
        TimeSpan shakeDuration;
        float shakeTime;
        float shakeAngle;

        /// <summary>
        /// Causes the StarShip to shake.
        /// </summary>
        /// <param name="shakeMagnitude">The magnitude of the shake.</param>
        /// <param name="shakeDuration">The duration of the shake.</param>
        public void Shake(float shakeMagnitude, TimeSpan shakeDuration)
        {
            this.shakeMagnitude = shakeMagnitude;
            this.shakeDuration = shakeDuration;
            shakeTime = 0.0f;
            isShake = true;
        }

        #endregion

        #region Constructor

        /// <summary>
        /// Initializes a new instance of this class.
        /// </summary>
        /// <param name="input">StarShip input management functions.</param>
        /// <param name="playerIndex">Specifies which player to read input from.</param>
        public StarShipRotation(IShipInput input, PlayerIndex playerIndex)
        {
            this.input = input;
            this.playerIndex = playerIndex;
        }

        #endregion

        #region Rotation

        /// <summary>
        /// Computes the StarShip's rotation. 
        /// </summary>
        /// <param name="gameTime">Time elapsed since the last call to this method.</param>
        /// <returns>StarShip's rotation.</returns>
        public Matrix ComputeShipRotation(GameTime gameTime)
        {
            #region Testing
#if DEBUG
            if (input.IsResetShipRotation())
            {
                // reset all matrices
                rotMatrix = Matrix.Identity;

                // reset rotation angles
                pitchAngle = 0.0f;
                yawAngle = 0.0f;
                rollAngle = 0.0f;
                rockAngle = 0.0f;

                return rotMatrix;
            }
#endif
            #endregion

            YawControl(gameTime);
            PitchControl(gameTime);
            RollControl(gameTime);
            RockRollControl(gameTime);
            ShakeControl(gameTime);
            UpdateShipRotMatrix();

            return rotMatrix;
        }

        private void YawControl(GameTime gameTime)
        {
            #region Yaw Control

            // rotate left around yaw axis
            if (input.IsYawLeft(playerIndex))
            {
                #region Yaw Left

                float timeDelta = (float)(gameTime.ElapsedGameTime.TotalMilliseconds / PlayYawTime.TotalMilliseconds);
                yawAngle += MaxYawAngle * timeDelta;
                yawAngle = MathHelper.Clamp(yawAngle, -MaxYawAngle, MaxYawAngle);

                #endregion
            }
            // rotate right around yaw axis                    
            else if (input.IsYawRight(playerIndex))
            {
                #region Yaw Right

                float timeDelta = (float)(gameTime.ElapsedGameTime.TotalMilliseconds / PlayYawTime.TotalMilliseconds);
                yawAngle -= MaxYawAngle * timeDelta;
                yawAngle = MathHelper.Clamp(yawAngle, -MaxYawAngle, MaxYawAngle);

                #endregion
            }
            // reach back idle yaw rotation angle
            else
            {
                #region Idle

                if (yawAngle > 0.0f)
                {
                    float timeDelta = (float)(gameTime.ElapsedGameTime.TotalMilliseconds / IdleYawTime.TotalMilliseconds);
                    yawAngle -= MaxYawAngle * timeDelta;
                    yawAngle = MathHelper.Clamp(yawAngle, 0.0f, MaxYawAngle);
                }
                else if (yawAngle < 0.0f)
                {
                    float timeDelta = (float)(gameTime.ElapsedGameTime.TotalMilliseconds / IdleYawTime.TotalMilliseconds);
                    yawAngle += MaxYawAngle * timeDelta;
                    yawAngle = MathHelper.Clamp(yawAngle, -MaxYawAngle, 0.0f);
                }

                #endregion
            }

            #endregion
        }

        private void PitchControl(GameTime gameTime)
        {
            #region Pitch Control

            // rotate up around pitch axis
            if (input.IsPitchUp(playerIndex))
            {
                #region Pitch Up

                float timeDelta = (float)(gameTime.ElapsedGameTime.TotalMilliseconds / PlayPitchTime.TotalMilliseconds);
                pitchAngle += MaxPitchAngle * timeDelta;
                pitchAngle = MathHelper.Clamp(pitchAngle, -MaxPitchAngle, MaxPitchAngle);

                #endregion
            }
            // rotate down around pitch axis
            else if (input.IsPitchDown(playerIndex))
            {
                #region Pitch Down

                float timeDelta = (float)(gameTime.ElapsedGameTime.TotalMilliseconds / PlayPitchTime.TotalMilliseconds);
                pitchAngle -= MaxPitchAngle * timeDelta;
                pitchAngle = MathHelper.Clamp(pitchAngle, -MaxPitchAngle, MaxPitchAngle);

                #endregion
            }
            // reach back idle pitch rotation angle
            else
            {
                #region Idle

                if (pitchAngle > 0.0f)
                {
                    float timeDelta = (float)(gameTime.ElapsedGameTime.TotalMilliseconds / IdlePitchTime.TotalMilliseconds);
                    pitchAngle -= MaxPitchAngle * timeDelta;
                    pitchAngle = MathHelper.Clamp(pitchAngle, 0.0f, MaxPitchAngle);
                }
                else if (pitchAngle < 0.0f)
                {
                    float timeDelta = (float)(gameTime.ElapsedGameTime.TotalMilliseconds / IdlePitchTime.TotalMilliseconds);
                    pitchAngle += MaxPitchAngle * timeDelta;
                    pitchAngle = MathHelper.Clamp(pitchAngle, -MaxPitchAngle, 0.0f);
                }

                #endregion
            }

            #endregion
        }

        private void RollControl(GameTime gameTime)
        {
            #region Roll Control

            // rotate right around roll axis
            if (input.IsRollRight(playerIndex))
            {
                #region Roll Right

                float timeDelta = (float)(gameTime.ElapsedGameTime.TotalMilliseconds / PlayRollTime.TotalMilliseconds);
                rollAngle -= MaxRollAngle * timeDelta;
                rollAngle = MathHelper.Clamp(rollAngle, -MaxRollAngle, MaxRollAngle);

                #endregion
            }
            // rotate left around roll axis
            else if (input.IsRollLeft(playerIndex))
            {
                #region Roll Left

                float timeDelta = (float)(gameTime.ElapsedGameTime.TotalMilliseconds / PlayRollTime.TotalMilliseconds);
                rollAngle += MaxRollAngle * timeDelta;
                rollAngle = MathHelper.Clamp(rollAngle, -MaxRollAngle, MaxRollAngle);

                #endregion
            }
            // rotate left around both yaw and roll axes
            else if (input.IsYawLeft(playerIndex))
            {
                #region Yaw/Roll Left

                if (rollAngle > MaxRollYawAngle)
                {
                    float timeDelta = (float)(gameTime.ElapsedGameTime.TotalMilliseconds / IdleRollTime.TotalMilliseconds);
                    rollAngle -= MaxRollYawAngle * timeDelta;
                    rollAngle = MathHelper.Clamp(rollAngle, MaxRollYawAngle, MaxRollAngle);
                }
                else if (rollAngle <= MaxRollYawAngle)
                {
                    float timeDelta = (float)(gameTime.ElapsedGameTime.TotalMilliseconds / PlayYawTime.TotalMilliseconds);
                    rollAngle += MaxRollYawAngle * timeDelta;
                    rollAngle = MathHelper.Clamp(rollAngle, -MaxRollAngle, MaxRollYawAngle);
                }

                #endregion
            }
            // rotate right around both yaw and roll axes
            else if (input.IsYawRight(playerIndex))
            {
                #region Yaw/Roll Right

                if (rollAngle < -MaxRollYawAngle)
                {
                    float timeDelta = (float)(gameTime.ElapsedGameTime.TotalMilliseconds / IdleRollTime.TotalMilliseconds);
                    rollAngle += MaxRollYawAngle * timeDelta;
                    rollAngle = MathHelper.Clamp(rollAngle, -MaxRollAngle, -MaxRollYawAngle);
                }
                else if (rollAngle >= -MaxRollYawAngle)
                {
                    float timeDelta = (float)(gameTime.ElapsedGameTime.TotalMilliseconds / PlayYawTime.TotalMilliseconds);
                    rollAngle -= MaxRollYawAngle * timeDelta;
                    rollAngle = MathHelper.Clamp(rollAngle, -MaxRollYawAngle, MaxRollAngle);
                }

                #endregion
            }
            // reach back idle roll rotation angle
            else
            {
                #region Idle

                if (rollAngle > MaxRollYawAngle)
                {
                    float timeDelta = (float)(gameTime.ElapsedGameTime.TotalMilliseconds / IdleRollTime.TotalMilliseconds);
                    rollAngle -= MaxRollAngle * timeDelta;
                    rollAngle = MathHelper.Clamp(rollAngle, MaxRollYawAngle, MaxRollAngle);
                }
                else if (rollAngle > 0.0f && rollAngle <= MaxRollYawAngle)
                {
                    float timeDelta = (float)(gameTime.ElapsedGameTime.TotalMilliseconds / IdleYawTime.TotalMilliseconds);
                    rollAngle -= MaxRollYawAngle * timeDelta;
                    rollAngle = MathHelper.Clamp(rollAngle, 0.0f, MaxRollYawAngle);
                }
                else if (rollAngle < 0.0f && rollAngle >= -MaxRollYawAngle)
                {
                    float timeDelta = (float)(gameTime.ElapsedGameTime.TotalMilliseconds / IdleYawTime.TotalMilliseconds);
                    rollAngle += MaxRollYawAngle * timeDelta;
                    rollAngle = MathHelper.Clamp(rollAngle, -MaxRollYawAngle, 0.0f);
                }
                else if (rollAngle < -MaxRollYawAngle)
                {
                    float timeDelta = (float)(gameTime.ElapsedGameTime.TotalMilliseconds / IdleRollTime.TotalMilliseconds);
                    rollAngle += MaxRollAngle * timeDelta;
                    rollAngle = MathHelper.Clamp(rollAngle, -MaxRollAngle, -MaxRollYawAngle);
                }

                #endregion
            }

            #endregion
        }

        private void RockRollControl(GameTime gameTime)
        {
            #region RockRoll Control

            switch (rrState)
            {
                case (RockRollState.RR_RIGHT):
                    {
                        if (rockAngle > -RockRollAngle)
                        {
                            float timeDelta = (float)(gameTime.ElapsedGameTime.TotalMilliseconds / RockRollTime.TotalMilliseconds);
                            rockAngle -= RockRollAngle * timeDelta;
                            rockAngle = MathHelper.Clamp(rockAngle, -RockRollAngle, RockRollAngle);
                        }
                        else
                        {
                            rrState = RockRollState.RR_LEFT;
                        }

                        break;
                    }
                case (RockRollState.RR_LEFT):
                    {
                        if (rockAngle < RockRollAngle)
                        {
                            float timeDelta = (float)(gameTime.ElapsedGameTime.TotalMilliseconds / RockRollTime.TotalMilliseconds);
                            rockAngle += RockRollAngle * timeDelta;
                            rockAngle = MathHelper.Clamp(rockAngle, -RockRollAngle, RockRollAngle);
                        }
                        else
                        {
                            rrState = RockRollState.RR_RIGHT;
                        }

                        break;
                    }
                default:
                    {
                        break;
                    }
            }

            #endregion
        }

        private void ShakeControl(GameTime gameTime)
        {
            #region ShakeControl

            if (isShake)
            {
                shakeAngle = StarFoxHelper.RandomFloat(-MathHelper.PiOver2, MathHelper.PiOver2);
                shakeAngle *= shakeMagnitude * (1.0f - (float)Math.Pow(shakeTime, 2));

                // update timer
                float shakeDelta = (float)(gameTime.ElapsedGameTime.TotalMilliseconds / shakeDuration.TotalMilliseconds);
                shakeTime += shakeDelta;
                shakeTime = MathHelper.Clamp(shakeTime, 0.0f, 1.0f);
                if (shakeTime == 1.0f) { isShake = false; shakeTime = 0.0f; shakeAngle = 0.0f; }
            }

            #endregion
        }

        private void UpdateShipRotMatrix()
        {
            #region Matrix Computation

            // convert all angles to radians and compute matrices...
            // ... yaw
            float auxYawAngle = MathHelper.ToRadians(yawAngle);
            Matrix yawRotMatrix = Matrix.CreateRotationY(auxYawAngle);
            // ... pitch
            float auxPitchAngle = MathHelper.ToRadians(pitchAngle);
            Matrix pitchRotMatrix = Matrix.CreateRotationX(auxPitchAngle);
            // ... roll
            float auxRollAngle = MathHelper.ToRadians(rollAngle);
            Matrix rollRotMatrix = Matrix.CreateRotationZ(auxRollAngle);
            // ... rock
            float auxRockAngle = MathHelper.ToRadians(rockAngle);
            Matrix rockRotMatrix = Matrix.CreateRotationZ(auxRockAngle);
            // ... shake
            float auxShakeAngle = MathHelper.ToRadians(shakeAngle);
            Matrix shakeRotMatrix = Matrix.CreateRotationZ(auxShakeAngle);

            // proper order for matrix product
            rotMatrix = rollRotMatrix * rockRotMatrix * shakeRotMatrix * yawRotMatrix * pitchRotMatrix;

            #endregion
        }

        #endregion
    }
}
